<?
/**
 * Use this application to create html form objects. Here's an example:
 *
 * $form = new Spaph_Form('contacts');
 * $firstname = $form->addElement('text','firstname');
 * $firstname->addAttribute('class',array('mr','active');
 * $firstname->addAttribute('required','required');
 * $firstname->addAttribute('maxlength','25');
 * $firstname->setDefault('Bruce');
 *
 * // Then later in a template
 * $form->open();
 * <label>First name</lable> <?php echo($form->getElement('firstname')->render()); ?>
 * $form->close();
 *
 * // Then even later fetch the submitted value
 * $form->getElement('firstname')->value()
 *
 */
class Spaph_Form {
    private $elements = array();
    private $attributes = array();
    private $action = '';
    
    public function __construct($name)
    {
        $this->setAttribute('name',$name);
        $this->setAttribute('id',$name);
        
        $this->_init();
    }
    
    /**
     * Set default form attributes.
     */
    public function _init()
    {
        $this->setAttribute('action',Spaph::getInstance()->selfUrl());
        $this->setAttribute('method','post');
    }

    /**
     * Find out if the form is submitted yet. This just checks
     * Spaph::Request to see if it is empty.
     *
     * @return boolean
     */
    public function isSubmitted()
    {
        $req = new Spaph_Request();

        if($req->isEmpty()) {
            return false; // The Request object is empty, So not submitted.
        }
        else {
            return true;
        }
    }

    /**
     * Set/Get the form's action attribute.
     *
     * @param string $action
     * @return string
     */
    public function action($action=null)
    {
        // This might be a setter.
        if(isset($action))
            $this->setAttribute('action',$action);
        
        // If we have an action attribute then return it.
        if(isset($this->attributes['action']))
            return $this->getAttribute('action');
    }

    /**
     * Set the form's method attribute.
     *
     * @param string $action
     * @return string
     */
    public function method($method)
    {
        $this->setAttribute('method',$method);
    }

    /**
     * Get an opening <form> tag with all the correct attributes set.
     */
    public function open()
    {
        if(!$this->getAttribute('action'))
            throw new Exception('Cannot open form because the action attribute is not set.');
            
        $attributes = array();
        foreach($this->getAttributes() as $key) {
            $val = $this->getAttribute($key);
            
            $attributes[] = $key.'='.'"'.$val.'"';
        }
        
        $opening_tag = sprintf('<form %s>',implode(' ',$attributes));
        
        // This looks like a spaph controller form. So, let's assume we need a do action.
        if(Spaph::getInstance()->selfUrl() == $this->action()) {
            $spaph_action = Spaph::getInstance()->getAction();
            $spaph_param = Spaph::getInstance()->getActionParam();
            $this->addElement('hidden',$spaph_param)->setAttribute('value',implode('.',array_reverse($spaph_action)));
            $opening_tag .= "\n".$this->getElement($spaph_param)->render();
        }
        
        return $opening_tag;
    }
    
    /**
     * Get a closing </form> tag with a nice comment.
     */
    public function close()
    {
        return sprintf('</form> <!-- #%s -->',$this->getAttribute('name'));
    }

    /**
     * Set all attributes using this method. An array can be used
     * to set a bunch of attributes with one call. The $val will
     * most likely be a string. Currently, only the "class" 
     * attribute can handle arrays correctly.
     *
     * @param string|array $key
     * @param mixed $val
     */
    public function setAttribute($key, $val=null)
    {
        // $key might be an array.
        if(is_array($key) && is_null($val)) {
            foreach($key as $k => $v) {
                $this->setAttribute($k,$v);
            }
        }
        
        if(is_null($val))
            throw new Exception('The value cannot be null for '.$key);
        
        $this->attributes[$key] = $val;
    }
    
    /**
     * Get the value of an attribute, like name or class etc.
     *
     * @param string $key
     */
    public function getAttribute($key)
    {
        if(!isset($this->attributes[$key]))
            throw new Exception('Cannot find attribute for '.$key);
        
        // The "class" attribute can be an array, so return string.
        if(is_array($this->attributes[$key])) {
            return implode(' ',$this->attributes[$key]);
        }
        
        return $this->attributes[$key];
    }
    
    /**
     * Access the list of attributes for a given element.
     *
     * @return array attribute keys
     */
    public function getAttributes()
    {
        $attribute_keys = array_keys($this->attributes);
        sort($attribute_keys);
        return $attribute_keys;
    }
    
    /**
     * This will append classes to the class attribute.
     *
     * @param string $class
     */
    public function addClass($class)
    {
        if(isset($this->attributes['class'])) {
            $this->attributes['class'][] = $class;
        }
        else {
            $this->attributes['class'] = array($class);
        }
    }
    
    /**
     * Accessor method to attributes['name']
     *
     * @return string
     */
    public function name() 
    {
        return $this->getAttribute('name');
    }

    /**
     * Accessor method to attributes['id']
     *
     * @return string
     */
    public function id() 
    {
        return $this->getAttribute('id');
    }

    /**
     * This a factory constructor. Request a type of Form Element.
     *
     * @param string $type
     * @param string $name
     * @return object
     */
    public function addElement($type, $name)
    {
        if(isset($this->elements[$name]))
            throw new Exception('The element ('.$name.') already exists!');
            
        switch($type) {
            case 'checkbox':
                $el = new Spaph_Form_Element_Checkbox($name);        
                break;
            case 'text':
                $el = new Spaph_Form_Element_Text($name);        
                break;
            case 'hidden':
                $el = new Spaph_Form_Element_Hidden($name);        
                break;
            case 'select':
                $el = new Spaph_Form_Element_Select($name);
                break;
            case 'submit':
                $el = new Spaph_Form_Element_Submit($name);
                break;
            default:
                throw new Exception('Cannot create an unknown element, '.$type);
        }
        
        $this->elements[$name] = $el;
        
        return $el;
    }
    
    /**
     * Get a form element object.
     *
     * @param string $name
     * @return object Spaph_Form_Element_*
     */
    public function getElement($name)
    {
        if(!isset($this->elements[$name]))
            throw new Exception('The element ('.$name.') does not exist!');
            
        return $this->elements[$name];
    }

}

class Spaph_Form_Element extends Spaph_Form {
    
    private $isRequired = false;
    
    public function _init() {  }
    
    /**
     * Accessor
     *
     * @return string
     */
    public function getValue() 
    {
        return $this->getAttribute('value');
    }
    
    /**
     * Is the element required during validation?
     */
    public function setRequired($bool)
    {
        $this->isRequired = $bool;
    }
}

/**
 * Make checkbox
 */
class Spaph_Form_Element_Checkbox extends Spaph_Form_Element {
    public function _init()
    {
        $this->setAttribute('type', 'checkbox');
    }
    
    public function render()
    {
        $attributes = array();
        foreach($this->getAttributes() as $key) {          
            if('value' == $key) {
                $attributes[] = $key.'='.'"'.htmlspecialchars($this->getAttribute($key)).'"';
            }
            else {
                $attributes[] = $key.'='.'"'.$this->getAttribute($key).'"';
            }
        }
        
        return sprintf('<input %s />',implode(' ',$attributes));
    }
}

/**
 * Make textbox
 */
class Spaph_Form_Element_Text extends Spaph_Form_Element {
    public function _init()
    {
        $req = new Spaph_Request();
        $this->setAttribute('value', $req->get($this->name()));
        $this->setAttribute('type', 'text');
    }
    
    public function setDefault($value)
    {
        $req = new Spaph_Request();
        if($req->get($this->name()))
            return;
        
        $this->setAttribute('value',$value);
    }
    
    public function render()
    {
        $attributes = array();
        foreach($this->getAttributes() as $key) {          
            if('value' == $key) {
                $attributes[] = $key.'='.'"'.htmlspecialchars($this->getAttribute($key)).'"';
            }
            else {
                $attributes[] = $key.'='.'"'.$this->getAttribute($key).'"';
            }
        }
        
        return sprintf('<input %s />',implode(' ',$attributes));
    }
}

/**
 * Make hidden
 */
class Spaph_Form_Element_Hidden extends Spaph_Form_Element_Text {
    public function _init()
    {
        $req = new Spaph_Request();
        $this->setAttribute('value', $req->get($this->name()));
        $this->setAttribute('type', 'hidden');
    }
}

/**
 * Make submit button
 */
class Spaph_Form_Element_Submit extends Spaph_Form_Element {    
    public function _init()
    {
        $req = new Spaph_Request();
        $this->setAttribute('value', 'Submit');
        $this->setAttribute('type', 'submit');
    }
    
    public function render()
    {
        $attributes = array();
        foreach($this->getAttributes() as $key) {          
            $attributes[] = $key.'='.'"'.$this->getAttribute($key).'"';
        }
        
        return sprintf('<input %s />',implode(' ',$attributes));
    }
}

/**
 * Make select tag
 */
class Spaph_Form_Element_Select extends Spaph_Form_Element {
    private $options = array();
    private $option_cnt = 0;
    
    public function addOption($name, $value, $group=null)
    {
        $req = new Spaph_Request();
        
        if(isset($group)) {
            $this->options[$group][] = array($name, $value);
        }
        else {
            $this->options['__nothing'][] = array($name, $value);
        }
    }
    
    public function render()
    {
        $attributes = array();
        foreach($this->getAttributes() as $key) {
            $attributes[] = $key.'='.'"'.$this->getAttribute($key).'"';
        }
        
        $buffer = array();
        $buffer[] = sprintf('<select %s>',implode(' ',$attributes));
        
        if(isset($this->options['__nothing'])) {
            foreach($this->options['__nothing'] as $o) {
                $buffer[] = $this->build_option_tag_str($o);
            }
        }
        else {
            // Don't need this then.            
            foreach($this->options as $group => $options) {
                $buffer[] = sprintf('<optgroup label="%s">',$group);
                foreach($options as $o) { 
                    $buffer[] = $this->build_option_tag_str($o);
                }
                $buffer[] = '</optgroup>';
            }
        }

        $buffer[] = '</select>';
        
        return implode("\n",$buffer);
    }
    
    private function build_option_tag_str($arr)
    {
        $req = new Spaph_Request();
        
        if($arr[1] == $req->get($this->name())) {
            return sprintf('<option value="%s" class="opt-%s" selected="selected">%s</option>',htmlspecialchars($arr[1]), $this->option_cnt++, htmlspecialchars($arr[0]));
        }
        else {
            return sprintf('<option value="%s" class="opt-%s">%s</option>',htmlspecialchars($arr[1]),$this->option_cnt++, htmlspecialchars($arr[0]));
        }
    }
}